use super::{
    err, err_value, get_cfg, json, load_box, load_ring, ok, save_cfg, save_data, throw_err, Base,
    Client, ECode, Lock, MapSv, RNull, Resp, TBase, VBase, VMap, Value, VecV, SS,
};
use std::collections::{HashMap, HashSet};
use std::f64::consts::PI;
use tracing::{instrument, warn};

type BFF = (bool, f64, f64);

pub struct Group;

impl Base for Group {}
impl TBase for Group {}

impl Group {
    pub async fn do_cmd(&self, para: &Value, cmd: &str, client: &Client, lock: Lock) -> Resp {
        let data = para.ckobj()?;
        let line = data.ck_str("line")?;

        match cmd {
            "any" => self.do_any(line, data, client, lock).await,
            "pair" => self.do_pair(line, data, client, lock).await,
            "load" => self.do_load(line, data, client).await,
            "sync" => self.do_sync(line, data, client).await,
            _ => throw_err!(ECode::NoServe, "no group serve"),
        }
    }

    #[instrument(skip_all, name = "do_sync")]
    async fn do_sync(&self, line: &str, data: &MapSv, client: &Client) -> Resp {
        let tsn = data.ck_str("tsn")?;

        self.calc(line, tsn, client).await?;

        ok!()
    }

    #[instrument(skip_all, name = "do_load")]
    async fn do_load(&self, line: &str, data: &MapSv, client: &Client) -> Resp {
        let tp = data.ck_str("k")?;

        let fname = match tp {
            "any" => "ga_pair",
            "pair" => "gp_pair",
            _ => throw_err!(ECode::Para, "tp error"),
        };

        let cmd = json!({
            "line": line,
        });

        let body = client.cfgdb(&cmd, &format!("file/list/{fname}")).await?;
        let body = body.ck_array("data")?;

        let mut ret = Vec::new();

        for d in body.iter() {
            let tech = d.ck_str("sn")?;

            let val = match d.k_array("v") {
                Some(v) => v,
                None => {
                    continue;
                }
            };

            for p in val.iter() {
                let p = p.ckstr()?;

                let cmd = json!({
                    "line": line,
                    "k": p,
                });

                let psn_body = match client.cfgdb(&cmd, "psn/get").await {
                    Ok(v) => v,
                    Err(_) => {
                        continue;
                    }
                };

                let mut psn_body = psn_body.ck_obj("data")?.clone();

                psn_body.insert("tech".to_string(), json!(tech));
                ret.push(psn_body);
            }
        }

        if ret.is_empty() {
            return Err(json!("no data"));
        }

        ok!(ret)
    }

    #[instrument(skip_all, name = "ck_size")]
    fn ck_size(&self, dr: &BFF, kr: &BFF, val: &MapSv) -> RNull {
        let outdia = val.ck_f64("outdia")? / 1000.0;
        let india = val.ck_f64("india")? / 1000.0;

        if dr.0 {
            if !self.is_range(outdia, dr.1, dr.2) {
                return Err(json!(""));
            }
        }

        if kr.0 {
            if !self.is_range(india, kr.1, kr.2) {
                return Err(json!(""));
            }
        }

        Ok(())
    }

    #[instrument(skip_all, name = "calc")]
    async fn calc(&self, line: &str, tsn: &str, client: &Client) -> Result<(), Value> {
        warn!("group step: get tech");

        let cmd = json!({
            "line": line,
            "k": tsn,
        });

        let tech = client.cfgdb(&cmd, "tech/get").await?;
        let tech = tech.ck_obj("data")?;

        let outdiff = tech.ck_f64("outdiff")?;
        let angdiff = tech.ck_f64("angdiff")?;
        let turn = tech.ck_i64("turn")? as f64;
        let ball = tech.ck_f64("ball")?;
        let b_india = tech.ck_f64("india")?;
        let b_outdia = tech.ck_f64("outdia")?;
        let b_angle = tech.ck_f64("angle")?;

        let ma = match tech.k_f64("ma") {
            Some(v) => v,
            None => {
                let m = (b_india + b_outdia) / 2.0;

                let tmp = b_angle * PI / 180.0;

                let val = 0.5 * (1.0 - ball * tmp.cos() / m);

                let ret = if val >= 0.45 { 0.45 } else { 0.4 };

                let cmd = json!({
                    "line": line,
                    "v": {
                        "sn": tsn,
                        "ma": ret,
                    }
                });

                let _ = client.cfgdb(&cmd, "tech/mditem").await;

                ret
            }
        };

        warn!("group step: load data");

        let _ = self.do_data(line, client).await;

        warn!("group step: load group");

        let psn_set = load_ring("group", tsn, line, client, None).await?;
        let mut md = Vec::new();

        warn!("group step: calc");

        for (psn, psn_data) in psn_set.iter() {
            let psn_data = psn_data.ckobj()?;

            let cmd = json!({
                "line": line,
                "k": psn,
            });

            let pd = match client.cfgdb(&cmd, "psn/get").await {
                Ok(v) => v,
                Err(_) => {
                    continue;
                }
            };
            let pd = pd.ck_obj("data")?;

            let outwidth = match pd.k_f64("outwidth") {
                Some(v) => v,
                None => {
                    warn!("no outwidth: {psn}");
                    continue;
                }
            };
            let inwidth = match pd.k_f64("inwidth") {
                Some(v) => v,
                None => {
                    warn!("no inwidth: {psn}");
                    continue;
                }
            };

            let mut val = HashMap::new();
            val.insert("sn", json!(psn));

            match psn_data
                .k_f64("handstandout")
                .or(psn_data.k_f64("standout"))
            {
                Some(standout) => {
                    let b = standout + outdiff;
                    warn!("b: {:?}", b);

                    val.insert("standout", json!(b));
                    val.insert("dent", json!(outwidth - inwidth + b));
                }
                None => warn!("no standout: {psn}"),
            }

            let angle = match psn_data.k_f64("handangle") {
                Some(v) => {
                    let m = (b_india + b_outdia) / 2.0;
                    let tmp = m * (1.0 - 2.0 * (turn * ma + v / 360.0) / turn) / ball;

                    Some(tmp.acos() * 180.0 / PI + angdiff)
                }
                None => psn_data.k_f64("angle").map(|x| x + angdiff),
            };

            if let Some(v) = angle {
                warn!("angle: {:?}", v);
                val.insert("angle", json!(v));
                val.insert("b_angle", json!(b_angle));
            } else {
                warn!("no angle: {psn}");
            }

            match psn_data.k_obj("rotate") {
                Some(rotate) => {
                    let kia = rotate.ck_f64("kia")?;
                    let sia = rotate.ck_f64("sia")?;
                    let kea = rotate.ck_f64("kea")?;
                    let sea = rotate.ck_f64("sea")?;

                    val.insert("kia", json!(kia));
                    val.insert("sia", json!(sia));
                    val.insert("kea", json!(kea));
                    val.insert("sea", json!(sea));
                }
                None => warn!("no rotate: {psn}"),
            }

            md.push(val);
        } // for

        if !md.is_empty() {
            let cmd = json!({
                "line": line,
                "v": md
            });

            let _ = client.cfgdb(&cmd, &format!("file/rmset/group_{tsn}")).await;
            let _ = client.cfgdb(&cmd, "psn/mmd").await;
        }

        Ok(())
    }

    #[instrument(skip_all, name = "clear_pair")]
    async fn clear_pair(
        &self,
        line: &str,
        tsn: &str,
        pbox: &HashSet<String>,
        client: &Client,
    ) -> RNull {
        let mut md = Vec::new();

        let body = if pbox.is_empty() {
            self.get_run_psn(line, tsn, client).await?
        } else {
            self.get_box_data(line, pbox, client).await?
        };

        for pd in body.iter() {
            md.push(json!({
                "sn": pd.ck_str("sn")?,
                "group": "",
            }));
        }

        if !md.is_empty() {
            warn!("clear all group");

            let cmd = json!({
                "line": line,
                "v": md
            });

            let _ = client.cfgdb(&cmd, "psn/mmd").await;
        }

        warn!("clear gp_pair, ga_pair");

        let cmd = json!({
            "line": line,
            "sn": tsn,
        });

        let _ = client.cfgdb(&cmd, "file/rm/gp_pair").await?;
        let _ = client.cfgdb(&cmd, "file/rm/ga_pair").await?;

        Ok(())
    }

    #[instrument(skip_all, name = "do_pair")]
    async fn do_pair(&self, line: &str, data: &MapSv, client: &Client, lock: Lock) -> Resp {
        let _ = lock.lock().await;

        let tsn = data.ck_str("tsn")?;
        let br = self.get_range(data, "bmin", "bmax");
        let dr = self.get_range(data, "dmin", "dmax");
        let kr = self.get_range(data, "kmin", "kmax");
        let dent_diff = data.ck_f64("diff")?;
        let angle_diff = data.ck_f64("adiff")?;
        let pbox = match data.k_bool("pbox") {
            Some(v) => v,
            None => false,
        };
        let usn = match data.k_str_empty_trim("usn") {
            Some(v) => v,
            None => "",
        };

        let mut pbox_body = HashSet::new();
        if pbox {
            warn!("group step: load pbox");

            let _ = load_box(line, &mut pbox_body, &format!("gpbox{usn}"), client).await;

            if pbox_body.is_empty() {
                return ok!();
            }
        }

        let _ = self.clear_pair(line, &tsn, &pbox_body, client).await;

        warn!("group step: start calc");

        self.calc(line, tsn, client).await?;

        warn!("group step: geta all run psn");

        let psn_data = if pbox {
            self.get_box_data(line, &pbox_body, client).await?
        } else {
            self.get_run_psn(line, tsn, client).await?
        };

        let mut md = Vec::new();
        let mut md_psn = Vec::new();
        let mut set = HashSet::new();

        warn!("group step: start pair");

        for pd in psn_data.iter() {
            let pd = pd.ckobj()?;
            let psn = pd.ck_str("sn")?;

            if set.contains(psn) {
                continue;
            }

            let india = pd.ck_f64("india")?;

            if let None = pd.k_f64("kia") {
                continue;
            }

            let standout = match pd.k_f64("standout") {
                Some(v) => v,
                None => {
                    continue;
                }
            };

            let angle = match pd.k_f64("angle") {
                Some(v) => v,
                None => {
                    continue;
                }
            };

            if let Err(_) = self.ck_size(&dr, &kr, pd) {
                continue;
            }

            for sub in psn_data.iter() {
                let sub = sub.ckobj()?;
                let sub_psn = sub.ck_str("sn")?;

                if sub_psn == psn {
                    continue;
                }

                if set.contains(sub_psn) {
                    continue;
                }

                if let None = sub.k_f64("kia") {
                    continue;
                }

                let sub_india = sub.ck_f64("india")?;

                let sub_standout = match sub.k_f64("standout") {
                    Some(v) => v,
                    None => {
                        continue;
                    }
                };

                let sub_angle = match sub.k_f64("angle") {
                    Some(v) => v,
                    None => {
                        continue;
                    }
                };
                let sub_dent = match sub.k_f64("dent") {
                    Some(v) => v,
                    None => {
                        continue;
                    }
                };

                if !self.is_eq(sub_india, india) {
                    continue;
                }

                if br.0 {
                    if !self.is_range(sub_standout / 1000.0, br.1, br.2) {
                        continue;
                    }
                }

                if let Err(_) = self.ck_size(&dr, &kr, sub) {
                    continue;
                }

                let tmp = (standout - sub_dent).abs();
                if !self.is_le(tmp, dent_diff) {
                    continue;
                }

                let tmp = (angle - sub_angle).abs();
                if !self.is_le(tmp, angle_diff) {
                    continue;
                }

                set.insert(sub_psn.to_string());
                set.insert(psn.to_string());

                let cmd = json!({
                    "line": line,
                    "rule": "group",
                });
                let body = client.cfgdb(&cmd, "code/gen").await?;
                let group_sn = body.ck_obj("data")?.ck_str("psn")?;

                md_psn.push(psn);
                md_psn.push(sub_psn);

                md.push(json!({
                    "sn": psn,
                    "group": "pair",
                    "groupsn": group_sn,
                    "other": sub_psn,
                    "gtp": "A",
                }));

                md.push(json!({
                    "sn": sub_psn,
                    "group": "pair",
                    "groupsn": group_sn,
                    "other": psn,
                    "gtp": "B",
                }));

                break;
            } // for
        } // for

        if !md.is_empty() {
            warn!("group step: update psn");

            let cmd = json!({
                "line": line,
                "v": md
            });

            let _ = client.cfgdb(&cmd, "psn/mmd").await?;

            warn!("group step: save gp_pair");

            let cmd = json!({
                "line": line,
                "sn": tsn,
                "v": {
                    "sn": tsn,
                    "v": md_psn,
                }
            });

            let _ = client.cfgdb(&cmd, "file/md/gp_pair").await?;
        }

        ok!()
    }

    async fn load_task(
        &self,
        line: &str,
        outer: &mut HashSet<String>,
        fname: &str,
        client: &Client,
    ) -> RNull {
        let cmd = json!({ "line": line });

        let cont = client.cfgdb(&cmd, &format!("field/list/{fname}")).await?;
        let cont = cont.ck_array("data")?;

        for k in cont.iter() {
            let k = k.ckstr()?;
            let cmd = json!({
                "line": line,
                "k": k,
            });

            let body = match client.cfgdb(&cmd, "psn/list").await {
                Ok(v) => v,
                Err(_) => {
                    continue;
                }
            };
            let body = body.ck_array("data")?;

            for d in body.iter() {
                let sn = d.ck_str("sn")?;
                outer.insert(sn.to_string());
            }
        }

        Ok(())
    }

    #[instrument(skip_all, name = "do_any")]
    async fn do_any(&self, line: &str, data: &MapSv, client: &Client, lock: Lock) -> Resp {
        let _ = lock.lock().await;

        let w_min = 0.0;
        let w_max = 1.0;
        let w = data.ck_f64("w")?;

        let tsn = data.ck_str("tsn")?;
        let br = self.get_range(data, "bmin", "bmax");
        let dr = self.get_range(data, "dmin", "dmax");
        let kr = self.get_range(data, "kmin", "kmax");
        let ar = self.get_range(data, "amin", "amax");
        let pbox = match data.k_bool("pbox") {
            Some(v) => v,
            None => false,
        };
        let usn = match data.k_str_empty_trim("usn") {
            Some(v) => v,
            None => "",
        };

        let ptask = match data.k_bool("ptask") {
            Some(v) => v,
            None => false,
        };

        let mut pbox_body = HashSet::new();

        if pbox {
            warn!("group any step: load pbox");

            let _ = load_box(line, &mut pbox_body, &format!("gabox{usn}"), client).await;

            if pbox_body.is_empty() {
                return ok!();
            }

            warn!("group any step: pbox: {:?}", pbox_body.len());
        }

        if ptask {
            warn!("group any step: load ptask");

            let mut ptask_body = HashSet::new();
            let _ = self
                .load_task(line, &mut ptask_body, "gatask", client)
                .await;

            if ptask_body.is_empty() {
                return ok!();
            }

            warn!("group any step: ptask: {:?}", ptask_body.len());

            if pbox_body.is_empty() {
                pbox_body = ptask_body;
            } else {
                let mut tmp = HashSet::new();
                for x in pbox_body.intersection(&ptask_body) {
                    tmp.insert(x.to_string());
                }

                pbox_body = tmp;
            }
        }

        warn!("group any step: pbox: {:?}", pbox_body.len());

        warn!("group any step: save calc para");

        let cmd = json!({
            "line": line,
            "sn": tsn,
            "v": data,
        });

        let _ = client.cfgdb(&cmd, "file/md/handany").await;

        let _ = self.clear_pair(line, &tsn, &pbox_body, client).await;

        warn!("group any step: start calc");

        self.calc(line, tsn, client).await?;

        warn!("group any step: geta all run psn");

        let psn_data = if pbox_body.is_empty() {
            self.get_run_psn(line, tsn, client).await?
        } else {
            self.get_box_data(line, &pbox_body, client).await?
        };

        let mut md = Vec::new();
        let mut md_psn = Vec::new();

        warn!("group any step: start pair");

        let mut all_log = Vec::new();

        all_log.push(format!("psn_data: {:?}", psn_data.len()));

        for pd in psn_data.iter() {
            let pd = pd.ckobj()?;

            let psn = pd.ck_str("sn")?;

            let standout = match pd.k_f64("standout") {
                Some(v) => v,
                None => {
                    all_log.push(format!("缺失突出量数据: {:?}", psn));
                    continue;
                }
            };

            if let None = pd.k_f64("kia") {
                all_log.push(format!("缺失旋转精度数据: {:?}", psn));
                continue;
            }

            let angle = match pd.k_f64("angle") {
                Some(v) => v,
                None => {
                    all_log.push(format!("缺失接触角数据: {:?}", psn));
                    continue;
                }
            };

            let outwidth = pd.ck_f64("outwidth")?;
            let inwidth = pd.ck_f64("inwidth")?;

            let w_diff = inwidth - outwidth;
            let tmp = w_diff.abs();

            if !self.is_le(tmp, w) {
                if !self.is_range(w_diff, w_min, w_max) {
                    all_log.push(format!(
                        "宽度差超限: {:?}, {:?}; {:?}, [{:?}, {:?}]",
                        tmp, w, w_diff, w_min, w_max
                    ));
                    continue;
                }
            }

            if br.0 {
                if !self.is_range(standout / 1000.0, br.1, br.2) {
                    all_log.push(format!(
                        "突出量超限: {:?}, {:?}, [{:?}, {:?}]",
                        psn, standout, br.1, br.2
                    ));
                    continue;
                }
            }

            if ar.0 {
                if let Some(b_angle) = pd.k_f64("angle") {
                    let t = angle - b_angle;

                    if !self.is_range(t / 1000.0, ar.1, ar.2) {
                        all_log.push(format!(
                            "接触角超限: {:?}, {:?}, {:?}, {:?}, [{:?}, {:?}]",
                            psn, angle, b_angle, t, ar.1, ar.2
                        ));
                        continue;
                    }
                }
            }

            if let Err(_) = self.ck_size(&dr, &kr, pd) {
                all_log.push(format!("直径超限"));
                continue;
            }

            md_psn.push(psn);

            md.push(json!({
                "sn": psn,
                "group": "any",
                "gtp": "B",
            }));
        } // for

        let cmd = json!({
            "line": line,
            "v": all_log,
        });

        let _ = client
            .cfgdb(&cmd, &format!("field/mdnocache/ghc_{tsn}"))
            .await;

        if !md.is_empty() {
            warn!("group any step: update psn");

            let cmd = json!({
                "line": line,
                "v": md
            });

            let _ = client.cfgdb(&cmd, "psn/mmd").await?;

            warn!("group any step: save ga_pair");

            let cmd = json!({
                "line": line,
                "sn": tsn,
                "v": {
                    "sn": tsn,
                    "v": md_psn,
                }
            });

            let _ = client.cfgdb(&cmd, "file/md/ga_pair").await?;
        }

        ok!()
    }

    /* group_<tsn>.json
     *    psn: {
     *       "inwidth": xx,
     *       ...
     *    }
     */
    #[instrument(skip_all, name = "do_data")]
    async fn do_data(&self, line: &str, client: &Client) -> RNull {
        let mut outer = MapSv::new();

        if let Err(e) = self.load_data(line, &mut outer, client).await {
            warn!("group load data error: {:?}", e);
        }

        if let Err(e) = save_data(line, "group", &outer, client).await {
            warn!("group save group error: {:?}", e);
        }

        Ok(())
    }

    fn collect(&self, outer: &mut MapSv, tab: &str, data: &VecV) -> Result<(), Value> {
        for d in data.iter() {
            let psn = match d.k_str_empty_trim("psn") {
                Some(v) => v,
                None => {
                    continue;
                }
            };
            let tsn = d.ck_str("tsn")?;

            let val = if tab.ends_with("angle") {
                json!(d.ck_f64("angle")?)
            } else if tab.ends_with("rotate") {
                json!({
                    "kia": d.ck_f64("kia")?,
                    "sia": d.ck_f64("sia")?,
                    "kea": d.ck_f64("kea")?,
                    "sea": d.ck_f64("sea")?,
                })
            } else {
                json!(d.ck_f64("val")?)
            };

            self.insert_data(outer, tab, tsn, psn, val);
        }

        Ok(())
    }

    async fn load_data(&self, line: &str, outer: &mut MapSv, client: &Client) -> RNull {
        let mut cfg = SS::new();

        let _ = get_cfg("groupos", line, &mut cfg, client).await;

        let tab = vec!["angle", "standout", "handstandout", "handangle", "rotate"];

        for d in tab.iter() {
            if let Ok(body) = self.read_data(line, d, &cfg, client).await {
                let body = body.ck_obj("data")?.ck_obj(d)?;

                let end = body.ck_str("end")?;
                let data = body.ck_array("data")?;

                cfg.insert(d.to_string(), end.to_string());

                let _ = self.collect(outer, d, &data);
            }
        }

        let _ = save_cfg("groupos", line, &cfg, client).await;

        Ok(())
    }

    async fn read_data(&self, line: &str, tab: &str, cfg: &SS, client: &Client) -> Resp {
        let now = self.pre_cur_time(1000 * 60 * 5, "%Y-%m-%dT%H:%M:%SZ")?;
        let utc = self.utc_offset_from_zero();

        let cmd = if let Some(t) = cfg.k_str(tab) {
            json!({
                "n": tab,
                "obj": "*",
                "opt": {
                    "and": [
                    {"eq": "line", "v": "1"},
                    {"gt": "time", "v": t},
                    {"le": "time", "v": now, "utc": utc},
                    ]
                }
            })
        } else {
            json!({
                "n": tab,
                "obj": "*",
                "opt": {
                    "and": [
                    {"eq": "line", "v": line},
                    {"le": "time", "v": now, "utc": utc},
                    ]
                }
            })
        };

        client.timedb(&cmd, "get").await
    }

    async fn get_box_data(
        &self,
        line: &str,
        pbox: &HashSet<String>,
        client: &Client,
    ) -> Result<VecV, Value> {
        let mut ret = Vec::new();

        let mut set = HashSet::new();

        for d in pbox.iter() {
            let cmd = json!({
                "line": line,
                "k": d
            });

            if let Ok(body) = client.cfgdb(&cmd, "psn/get").await {
                let body = body.ck_obj("data")?;

                let task = body.ck_str("task")?;
                set.insert(task.to_string());

                ret.push(json!(body));
            }
        }

        let mut md = Vec::new();

        for task_sn in set.iter() {
            let cmd = json!({
                "line": line,
                "k": task_sn,
            });

            if let Ok(body) = client.cfgdb(&cmd, "task/get").await {
                let state = body.ck_obj("data")?.ck_str("state")?;

                if state != "open" {
                    md.push(json!({
                        "sn": task_sn,
                        "state": "open",
                    }));
                }
            }
        }

        if !md.is_empty() {
            let cmd = json!({
                "line": line,
                "v": md,
            });

            let _ = client.cfgdb(&cmd, "task/mmd").await?;
        }

        Ok(ret)
    }

    async fn get_run_psn(&self, line: &str, tsn: &str, client: &Client) -> Result<VecV, Value> {
        let cmd = json!({
            "line": line,
            "withpsn": true,
            "igs": ["G"],
        });
        let mut task_body = client.cfgdb(&cmd, "task/run").await?;
        let task_body = task_body.ck_array_mut("data")?;

        let mut psn_data = Vec::new();

        for task in task_body.iter_mut() {
            let tech = task.ck_str("tech")?;
            if tech != tsn {
                continue;
            }

            let mut psn_body = match task.k_array_mut("psn") {
                Some(v) => v,
                None => {
                    continue;
                }
            };

            psn_data.append(&mut psn_body);
        }

        if psn_data.is_empty() {
            return Err(json!("no data"));
        }

        Ok(psn_data)
    }
}
